feat: @discordjs/structures (#10900)

* chore: init /structures

* feat: base structure

* feat: initial structures design attempt

* refactor(Structure): use unknown to store in kData

* feat(Structure): add Invite

refactor(Structure): patch to _patch

* refactor: symbol names and override location

* fix: don't possibly return 0 if discord borks

Co-authored-by: Synbulat Biishev <signin@syjalo.dev>

* refactor: use getter value instead of api

Co-authored-by: Synbulat Biishev <signin@syjalo.dev>

* refactor: cache createdTimestamp value

Co-authored-by: Qjuh <76154676+Qjuh@users.noreply.github.com>

* docs: better docs for what's done so far

* feat: add Mixin

* refactor(User): remove bitfield getters and add displayName

* feat(structures): add Connection

* feat(structures): add Channel base

* refactor(Mixin): trace prototype chain, allow construction

* fix(structures): fix mixin behavior

* fix(structures): data optimization call behavior from perf testing

* feat: channel mixins

* chore: update deps

* feat: channels and mixins

* chore: more typeguard tests

* fix: tests and some other issues

* feat: add ChannelWebhookMixin

* fix: more tests

* chore: tests and docs

* chore: docs

* fix: remove unneccessary omitted

* chore: apply code suggestions

* refactor: change how extended invite works

* fix: type imports

* Apply suggestions from code review

Co-authored-by: Almeida <github@almeidx.dev>

* fix: tests

* chore: add jsdoc

* refactor: apply code suggestions

* fix: don't instantiate sub-structures

* fix: don't do null default twice

* chore: use formatters, add _cache

* chore: lockfile

* chore: move MixinTypes to declaratiion file

* fix: tests

* fix: don't include source d.ts files for docs

* feat: bitfields

* feat: more bitfields

* refactor: remove DirectoryChannel structure

* chore: apply suggestions from code review

* chore: remove unused import

* refactor: use symbol for mixin toJSON, remove _ prefix

* chore: apply suggestions from code review

* refactor: remove bitfield casts

* refactor: remove special case for threadchannel types

* fix: apply code review suggestions

* refactor: bitfields always store bigint

* fix: tests

* chore: apply suggestions from code review

* fix: lint

* refactor: conditional structuredClone

* Apply suggestions from code review

Co-authored-by: ckohen <chaikohen@gmail.com>

* fix: code review errors

* fix: lint

* chore: bump dtypes

* Update packages/structures/cliff.toml

Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>

* docs: link to VideoQualityMode

* chore: typo in comment

* chore: small nits in docs links

* chore: small nits

* docs: forgot one

* chore: update template

* chore: typos and things

* chore: apply suggestions from code review

* fix: tests and typeguards

* chore: don't clone appliedTags

* refactor: use a symbol for patch method

* fix: add missing readonly

* chore: remove todo comment

* refactor: use symbol for clone

* fix: add constraint to DataType

* chore: apply suggestions

* fix: dtypes bump

* chore: fix comment

* chore: add todo comment

* chore: mark bitfield as todo
chore: mark bit field as todo and edit readme

---------

Co-authored-by: ckohen <chaikohen@gmail.com>
Co-authored-by: Synbulat Biishev <signin@syjalo.dev>
Co-authored-by: Almeida <github@almeidx.dev>
Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
Qjuh
2025-07-12 20:24:30 +02:00
committed by GitHub
parent 591668099e
commit 3cff4d7412
89 changed files with 4593 additions and 15 deletions

View File

@@ -0,0 +1,179 @@
import { DataTemplatePropertyName, OptimizeDataPropertyName, type Structure } from './Structure.js';
import { kMixinConstruct, kMixinToJSON } from './utils/symbols.js';
export type Mixinable<ClassType> = new (...args: unknown[]) => ClassType;
export type MixinBase<BaseClass extends Structure<{}>> =
BaseClass extends Structure<infer DataType, infer Omitted> ? Structure<DataType, Omitted> : never;
/**
* Copies the prototype (getters, setters, and methods) of all mixins to the destination class.
* For type information see {@link MixinTypes}
*
* @param destination - The class to apply the mixins to, must extend the base that the mixins expect it to.
* @param mixins - Classes that contain "pure" prototypes to be copied on top of the destination class prototype
* @remarks All mixins should be "pure" in that they only contain getters, setters, and methods.
* The runtime code will only copy these, and adding properties to the class only results
* in the types of the mixed class being wrong.
* @example
* ```
* // Interface merging on the mixin to give type access to props on the base and kData that are available once copied
* interface TextMixin extends Channel {}
* class TextMixin {
* // Methods / getters
* }
*
* // Interface merging on the mixed class to give it accurate type information within the declaration and when instantiated
* interface TextChannel extends MixinTypes<Channel, [TextMixin]> {}
* class TextChannel extends Channel {}
*
* // Apply for runtime
* Mixin(TextChannel, [TextMixin])
* ```
* @typeParam DestinationClass - The class to be mixed, ensures that the mixins provided can be used with this destination
*/
export function Mixin<DestinationClass extends typeof Structure<{}>>(
destination: DestinationClass,
mixins: Mixinable<MixinBase<DestinationClass['prototype']>>[],
) {
const dataTemplates: Record<string, unknown>[] = [];
const dataOptimizations: ((data: unknown) => void)[] = [];
const enrichToJSONs: ((data: Partial<unknown>) => void)[] = [];
const constructors: ((data: Partial<unknown>) => void)[] = [];
for (const mixin of mixins) {
// The entire prototype chain, in reverse order, since we want to copy it all
const prototypeChain: MixinBase<DestinationClass['prototype']>[] = [];
let extendedClass = mixin;
while (extendedClass.prototype !== undefined) {
if (
DataTemplatePropertyName in extendedClass &&
typeof extendedClass.DataTemplate === 'object' &&
// eslint-disable-next-line no-eq-null, eqeqeq
extendedClass.DataTemplate != null
) {
dataTemplates.push(extendedClass.DataTemplate as Record<string, unknown>);
}
prototypeChain.unshift(extendedClass.prototype);
extendedClass = Object.getPrototypeOf(extendedClass);
}
for (const prototype of prototypeChain) {
// Symboled data isn't traversed by Object.entries, we can handle it here
if (prototype[kMixinConstruct]) {
constructors.push(prototype[kMixinConstruct]);
}
if (prototype[kMixinToJSON]) {
enrichToJSONs.push(prototype[kMixinToJSON]);
}
// Copy instance methods and setters / getters
const originalDescriptors = Object.getOwnPropertyDescriptors(prototype);
const usingDescriptors: { [prop: string]: PropertyDescriptor } = {};
for (const [prop, descriptor] of Object.entries(originalDescriptors)) {
// Drop constructor
if (['constructor'].includes(prop)) {
continue;
}
// Special case for optimize function, we want to combine these
if (prop === OptimizeDataPropertyName) {
if (typeof descriptor.value !== 'function')
throw new RangeError(`Expected ${prop} to be a function, received ${typeof descriptor.value} instead.`);
dataOptimizations.push(descriptor.value);
continue;
}
// Shouldn't be anything other than these without being instantiated, but just in case
if (
typeof descriptor.get !== 'undefined' ||
typeof descriptor.set !== 'undefined' ||
typeof descriptor.value === 'function'
) {
usingDescriptors[prop] = descriptor;
}
}
Object.defineProperties(destination.prototype, usingDescriptors);
}
}
// Set the function to call any mixed constructors
if (constructors.length > 0) {
Object.defineProperty(destination.prototype, kMixinConstruct, {
writable: true,
enumerable: false,
configurable: true,
// eslint-disable-next-line func-name-matching
value: function _mixinConstructors(data: Partial<unknown>) {
for (const construct of constructors) {
construct.call(this, data);
}
},
});
}
// Combine all optimizations into a single function
const baseOptimize = Object.getOwnPropertyDescriptor(destination, OptimizeDataPropertyName);
if (baseOptimize && typeof baseOptimize.value === 'function') {
// call base last (mimic constructor behavior)
dataOptimizations.push(baseOptimize.value);
}
const superOptimize = Object.getOwnPropertyDescriptor(
Object.getPrototypeOf(destination).prototype,
OptimizeDataPropertyName,
);
// the mixin base optimize should call super, so we can ignore the super in that case
if (!baseOptimize && superOptimize && typeof superOptimize.value === 'function') {
// call super first (mimic constructor behavior)
dataOptimizations.unshift(superOptimize.value);
}
// If there's more than one optimization or if there's an optimization that isn't on the destination (base)
if (dataOptimizations.length > 1 || (dataOptimizations.length === 1 && !baseOptimize)) {
Object.defineProperty(destination.prototype, OptimizeDataPropertyName, {
writable: true,
enumerable: false,
configurable: true,
// eslint-disable-next-line func-name-matching
value: function _mixinOptimizeData(data: unknown) {
for (const optimization of dataOptimizations) {
optimization.call(this, data);
}
},
});
}
if (enrichToJSONs.length > 0) {
Object.defineProperty(destination.prototype, kMixinToJSON, {
writable: true,
enumerable: false,
configurable: true,
// eslint-disable-next-line func-name-matching
value: function _mixinToJSON(data: Partial<unknown>) {
for (const enricher of enrichToJSONs) {
enricher.call(this, data);
}
},
});
}
// Copy the properties (setters) of each mixins template to the destinations template
if (dataTemplates.length > 0) {
if (!Object.getOwnPropertyDescriptor(destination, DataTemplatePropertyName)) {
Object.defineProperty(destination, DataTemplatePropertyName, {
value: Object.defineProperties({}, Object.getOwnPropertyDescriptors(destination[DataTemplatePropertyName])),
writable: true,
enumerable: true,
configurable: true,
});
}
for (const template of dataTemplates) {
Object.defineProperties(destination[DataTemplatePropertyName], Object.getOwnPropertyDescriptors(template));
}
}
}

23
packages/structures/src/MixinTypes.d.ts vendored Normal file
View File

@@ -0,0 +1,23 @@
import type { MixinBase } from './Mixin.js';
import type { Structure } from './Structure.js';
import type { kData, kMixinConstruct } from './utils/symbols.js';
import type { CollapseUnion, MergePrototypes } from './utils/types.js';
/**
* Type utility to provide accurate types for the runtime effects of {@link Mixin}
*
* @typeParam BaseClass - The class that is being directly extended, must match the class that the mixins are expecting
* @typeParam Mixins - The mixins that will be applied to this class via a {@link Mixin} call
*/
export type MixinTypes<BaseClass extends Structure<{}>, Mixins extends readonly MixinBase<BaseClass>[]> = CollapseUnion<
BaseClass extends Structure<infer DataType, infer Omitted>
? Mixins[number] extends Structure<DataType, Omitted>
? // prettier-ignore
Structure<DataType, Omitted>[typeof kData] extends
// @ts-expect-error kData is protected
Mixins[number][typeof kData]
? Omit<MergePrototypes<Mixins>, keyof BaseClass | typeof kMixinConstruct>
: never
: never
: never
>;

View File

@@ -0,0 +1,144 @@
import { kClone, kData, kMixinConstruct, kMixinToJSON, kPatch } from './utils/symbols.js';
import type { ReplaceOmittedWithUnknown } from './utils/types.js';
export const DataTemplatePropertyName = 'DataTemplate';
export const OptimizeDataPropertyName = 'optimizeData';
/**
* Represents a data model from the Discord API
*
* @privateRemarks
* Explanation of the type complexity surround Structure:
*
* There are two layers of Omitted generics, one here, which allows omitting things at the library level so we do not accidentally
* access them, in addition to whatever the user does at the layer above.
*
* The second layer, in the exported structure is effectively a type cast that allows the getters types to match whatever data template is used
*
* In order to safely set and access this data, the constructor and patch take data as "partial" and forcibly assigns it to kData. To accommodate this,
* kData stores properties as `unknown` when it is omitted, which allows accessing the property in getters even when it may not actually be present.
* This is the most technically correct way of representing the value, especially since there is no way to guarantee runtime matches the "type cast."
*/
export abstract class Structure<DataType extends {}, Omitted extends keyof DataType | '' = ''> {
/**
* A construct function used when mixing to allow mixins to set optimized property defaults
*
* @internal
* @remarks This should only be used to set defaults, setting optimized values should be done
* in the mixins `optimizeData` method, which will be called automatically.
* @param data - The full API data received by the Structure
*/
protected [kMixinConstruct]?(data: Partial<DataType>): void;
/**
* A function used when mixing to allow mixins to add properties to the result of toJSON
*
* @internal
* @remarks This should only be used to add properties that the mixin optimizes, if the raw
* JSON data is unchanged the property will already be returned.
* @param data - The result of the base class toJSON Structure before it gets returned
*/
protected [kMixinToJSON]?(data: Partial<DataType>): void;
/**
* The template used for removing data from the raw data stored for each Structure.
*
* @remarks This template should be overridden in all subclasses to provide more accurate type information.
* The template in the base {@link Structure} class will have no effect on most subclasses for this reason.
*/
protected static readonly DataTemplate: Record<string, unknown> = {};
/**
* @returns A cloned version of the data template, ready to create a new data object.
*/
private getDataTemplate() {
return Object.create((this.constructor as typeof Structure).DataTemplate);
}
/**
* The raw data from the API for this structure
*
* @internal
*/
protected [kData]: Readonly<ReplaceOmittedWithUnknown<Omitted, DataType>>;
/**
* Creates a new structure to represent API data
*
* @param data - the data from the API that this structure will represent
* @remarks To be made public in subclasses
* @internal
*/
public constructor(data: Readonly<Partial<DataType>>, ..._rest: unknown[]) {
this[kData] = Object.assign(this.getDataTemplate(), data);
this[kMixinConstruct]?.(data);
}
/**
* Patches the raw data of this object in place
*
* @param data - the updated data from the API to patch with
* @remarks To be made public in subclasses
* @returns this
* @internal
*/
protected [kPatch](data: Readonly<Partial<DataType>>): this {
this[kData] = Object.assign(this.getDataTemplate(), this[kData], data);
this.optimizeData(data);
return this;
}
/**
* Creates a clone of this structure
*
* @returns a clone of this
* @internal
*/
protected [kClone](patchPayload?: Readonly<Partial<DataType>>): typeof this {
const clone = this.toJSON();
// @ts-expect-error constructor is of abstract class is unknown
return new this.constructor(
// Ensure the ts-expect-error only applies to the constructor call
patchPayload ? Object.assign(clone, patchPayload) : clone,
);
}
/**
* Function called to ensure stored raw data is in optimized formats, used in tandem with a data template
*
* @example created_timestamp is an ISO string, this can be stored in optimized form as a number
* @param _data - the raw data received from the API to optimize
* @remarks Implementation to be done in subclasses and mixins where needed.
* For typescript users, mixins must use the closest ancestors access modifier.
* @remarks Automatically called in Structure[kPatch] but must be called manually in the constructor
* of any class implementing this method.
* @remarks Additionally, when implementing, ensure to call `super._optimizeData` if any class in the super chain aside
* from Structure contains an implementation.
* Note: mixins do not need to call super ever as the process of mixing walks the prototype chain.
* @virtual
* @internal
*/
protected optimizeData(_data: Partial<DataType>) {}
/**
* Transforms this object to its JSON format with raw API data (or close to it),
* automatically called by `JSON.stringify()` when this structure is stringified
*
* @remarks
* The type of this data is determined by omissions at runtime and is only guaranteed for default omissions
* @privateRemarks
* When omitting properties at the library level, this must be overridden to re-add those properties
*/
public toJSON(): DataType {
// This will be DataType provided nothing is omitted, when omits occur, subclass needs to overwrite this.
const data =
// Spread is way faster than structuredClone, but is shallow. So use it only if there is no nested objects
(
Object.values(this[kData]).some((value) => typeof value === 'object' && value !== null)
? structuredClone(this[kData])
: { ...this[kData] }
) as DataType;
this[kMixinToJSON]?.(data);
return data;
}
}

View File

@@ -0,0 +1,203 @@
import type { EnumLike, NonAbstract, RecursiveReadonlyArray } from '../utils/types.js';
// TODO: this currently is mostly copied from mainlib discord.js v14 and definitely needs a refactor in a later iteration
/**
* Data that can be resolved to give a bit field. This can be:
* A bit number (this can be a number literal or a value taken from {@link (BitField:class).Flags})
* A string bit number
* An instance of BitField
* An Array of BitFieldResolvable
*/
export type BitFieldResolvable<Flags extends string> =
| Flags
| Readonly<BitField<Flags>>
| RecursiveReadonlyArray<Flags | Readonly<BitField<Flags>> | bigint | number | `${bigint}`>
| bigint
| number
| `${bigint}`;
/**
* Data structure that makes it easy to interact with a bit field.
*/
export abstract class BitField<Flags extends string> {
/**
* Numeric bit field flags.
*
* @remarks Defined in extension classes
*/
public static readonly Flags: EnumLike<unknown, bigint | number> = {};
public static readonly DefaultBit: bigint = 0n;
/**
* Bitfield of the packed bits
*/
public bitField: bigint;
declare public ['constructor']: NonAbstract<typeof BitField<Flags>>;
/**
* @param bits - Bit(s) to read from
*/
public constructor(bits: BitFieldResolvable<Flags> = this.constructor.DefaultBit) {
this.bitField = this.constructor.resolve(bits);
}
/**
* Checks whether the bit field has a bit, or any of multiple bits.
*
* @param bit - Bit(s) to check for
* @returns Whether the bit field has the bit(s)
*/
public any(bit: BitFieldResolvable<Flags>) {
return (this.bitField & this.constructor.resolve(bit)) !== this.constructor.DefaultBit;
}
/**
* Checks if this bit field equals another
*
* @param bit - Bit(s) to check for
* @returns Whether this bit field equals the other
*/
public equals(bit: BitFieldResolvable<Flags>) {
return this.bitField === this.constructor.resolve(bit);
}
/**
* Checks whether the bit field has a bit, or multiple bits.
*
* @param bit - Bit(s) to check for
* @returns Whether the bit field has the bit(s)
*/
public has(bit: BitFieldResolvable<Flags>, ..._hasParams: unknown[]) {
const resolvedBit = this.constructor.resolve(bit);
return (this.bitField & resolvedBit) === resolvedBit;
}
/**
* Gets all given bits that are missing from the bit field.
*
* @param bits - Bit(s) to check for
* @param hasParams - Additional parameters for the has method, if any
* @returns A bit field containing the missing bits
*/
public missing(bits: BitFieldResolvable<Flags>, ...hasParams: readonly unknown[]) {
return new this.constructor(bits).remove(this).toArray(...hasParams);
}
/**
* Freezes these bits, making them immutable.
*
* @returns This bit field but frozen
*/
public freeze() {
return Object.freeze(this);
}
/**
* Adds bits to these ones.
*
* @param bits - Bits to add
* @returns These bits or new BitField if the instance is frozen.
*/
public add(...bits: BitFieldResolvable<Flags>[]) {
let total = this.constructor.DefaultBit;
for (const bit of bits) {
total |= this.constructor.resolve(bit);
}
if (Object.isFrozen(this)) return new this.constructor(this.bitField | total);
this.bitField |= total;
return this;
}
/**
* Removes bits from these.
*
* @param bits - Bits to remove
* @returns These bits or new BitField if the instance is frozen.
*/
public remove(...bits: BitFieldResolvable<Flags>[]) {
let total = this.constructor.DefaultBit;
for (const bit of bits) {
total |= this.constructor.resolve(bit);
}
if (Object.isFrozen(this)) return new this.constructor(this.bitField & ~total);
this.bitField &= ~total;
return this;
}
/**
* Gets an object mapping field names to a boolean indicating whether the bit is available.
*
* @param hasParams - Additional parameters for the has method, if any
* @returns An object mapping field names to a boolean indicating whether the bit is available
*/
public serialize(...hasParams: readonly unknown[]) {
const serialized: Partial<Record<keyof Flags, boolean>> = {};
for (const [flag, bit] of Object.entries(this.constructor.Flags)) {
if (Number.isNaN(Number(flag))) serialized[flag as keyof Flags] = this.has(bit as bigint | number, ...hasParams);
}
return serialized;
}
/**
* Gets an Array of bit field names based on the bits available.
*
* @param hasParams - Additional parameters for the has method, if any
* @returns An Array of bit field names
*/
public toArray(...hasParams: readonly unknown[]) {
return [...this[Symbol.iterator](...hasParams)];
}
public toJSON(asNumber?: boolean) {
if (asNumber) {
if (this.bitField > Number.MAX_SAFE_INTEGER) {
throw new RangeError(
`Cannot convert bitfield value ${this.bitField} to number, as it is bigger than ${Number.MAX_SAFE_INTEGER} (the maximum safe integer)`,
);
}
return Number(this.bitField);
}
return this.bitField.toString();
}
public valueOf() {
return this.bitField;
}
public *[Symbol.iterator](...hasParams: unknown[]) {
for (const bitName of Object.keys(this.constructor.Flags)) {
if (Number.isNaN(Number(bitName)) && this.has(bitName as Flags, ...hasParams)) yield bitName as Flags;
}
}
/**
* Resolves bit fields to their numeric form.
*
* @param bit - bit(s) to resolve
* @returns the numeric value of the bit fields
*/
public static resolve<Flags extends string = string>(bit: BitFieldResolvable<Flags>): bigint {
const DefaultBit = this.DefaultBit;
if (typeof bit === 'bigint' && bit >= DefaultBit) return bit;
if (typeof bit === 'number' && BigInt(bit) >= DefaultBit) return BigInt(bit);
if (bit instanceof BitField) return bit.bitField;
if (Array.isArray(bit)) {
return bit.map((bit_) => this.resolve(bit_)).reduce((prev, bit_) => prev | bit_, DefaultBit);
}
if (typeof bit === 'string') {
if (!Number.isNaN(Number(bit))) return BigInt(bit);
if (bit in this.Flags) return this.Flags[bit as keyof typeof this.Flags];
}
throw new Error(`BitFieldInvalid: ${JSON.stringify(bit)}`);
}
}

View File

@@ -0,0 +1,16 @@
import { ChannelFlags } from 'discord-api-types/v10';
import { BitField } from './BitField.js';
/**
* Data structure that makes it easy to interact with a {@link (Channel:class).flags} bitfield.
*/
export class ChannelFlagsBitField extends BitField<keyof ChannelFlags> {
/**
* Numeric guild channel flags.
*/
public static override readonly Flags = ChannelFlags;
public override toJSON() {
return super.toJSON(true);
}
}

View File

@@ -0,0 +1,76 @@
/* eslint-disable unicorn/consistent-function-scoping */
import { PermissionFlagsBits } from 'discord-api-types/v10';
import type { BitFieldResolvable } from './BitField.js';
import { BitField } from './BitField.js';
/**
* Data structure that makes it easy to interact with a permission bit field. All {@link GuildMember}s have a set of
* permissions in their guild, and each channel in the guild may also have {@link PermissionOverwrite}s for the member
* that override their default permissions.
*/
export class PermissionsBitField extends BitField<keyof typeof PermissionFlagsBits> {
/**
* Numeric permission flags.
*
* @see {@link https://discord.com/developers/docs/topics/permissions#permissions-bitwise-permission-flags}
*/
public static override Flags = PermissionFlagsBits;
/**
* Bit field representing every permission combined
*/
public static readonly All = Object.values(PermissionFlagsBits).reduce((all, perm) => all | perm, 0n);
/**
* Bit field representing the default permissions for users
*/
public static readonly Default = 104_324_673n;
/**
* Bit field representing the permissions required for moderators of stage channels
*/
public static readonly StageModerator =
PermissionFlagsBits.ManageChannels | PermissionFlagsBits.MuteMembers | PermissionFlagsBits.MoveMembers;
/**
* Gets all given bits that are missing from the bit field.
*
* @param bits - Bit(s) to check for
* @param checkAdmin - Whether to allow the administrator permission to override
* @returns A bit field containing the missing permissions
*/
public override missing(bits: BitFieldResolvable<keyof typeof PermissionFlagsBits>, checkAdmin = true) {
return checkAdmin && this.has(PermissionFlagsBits.Administrator) ? [] : super.missing(bits);
}
/**
* Checks whether the bit field has a permission, or any of multiple permissions.
*
* @param permission - Permission(s) to check for
* @param checkAdmin - Whether to allow the administrator permission to override
* @returns Whether the bit field has the permission(s)
*/
public override any(permission: BitFieldResolvable<keyof typeof PermissionFlagsBits>, checkAdmin = true) {
return (checkAdmin && super.has(PermissionFlagsBits.Administrator)) || super.any(permission);
}
/**
* Checks whether the bit field has a permission, or multiple permissions.
*
* @param permission - Permission(s) to check for
* @param checkAdmin - Whether to allow the administrator permission to override
* @returns Whether the bit field has the permission(s)
*/
public override has(permission: BitFieldResolvable<keyof typeof PermissionFlagsBits>, checkAdmin = true) {
return (checkAdmin && super.has(PermissionFlagsBits.Administrator)) || super.has(permission);
}
/**
* Gets an Array of bitfield names based on the permissions available.
*
* @returns An Array of permission names
*/
public override toArray() {
return super.toArray(false);
}
}

View File

@@ -0,0 +1,4 @@
export * from './BitField.js';
export * from './ChannelFlagsBitField.js';
export * from './PermissionsBitField.js';

View File

@@ -0,0 +1,46 @@
import type { APINewsChannel, ChannelType } from 'discord-api-types/v10';
import { Mixin } from '../Mixin.js';
import type { MixinTypes } from '../MixinTypes.d.ts';
import type { Partialize } from '../utils/types.js';
import { Channel } from './Channel.js';
import { ChannelParentMixin } from './mixins/ChannelParentMixin.js';
import { ChannelPermissionMixin } from './mixins/ChannelPermissionMixin.js';
import { ChannelPinMixin } from './mixins/ChannelPinMixin.js';
import { ChannelSlowmodeMixin } from './mixins/ChannelSlowmodeMixin.js';
import { ChannelTopicMixin } from './mixins/ChannelTopicMixin.js';
import { TextChannelMixin } from './mixins/TextChannelMixin.js';
export interface AnnouncementChannel<Omitted extends keyof APINewsChannel | '' = ''>
extends MixinTypes<
Channel<ChannelType.GuildAnnouncement>,
[
TextChannelMixin<ChannelType.GuildAnnouncement>,
ChannelParentMixin<ChannelType.GuildAnnouncement>,
ChannelPermissionMixin<ChannelType.GuildAnnouncement>,
ChannelPinMixin<ChannelType.GuildAnnouncement>,
ChannelSlowmodeMixin<ChannelType.GuildAnnouncement>,
ChannelTopicMixin<ChannelType.GuildAnnouncement>,
]
> {}
/**
* Sample Implementation of a structure for announcement channels, usable by direct end consumers.
*/
export class AnnouncementChannel<Omitted extends keyof APINewsChannel | '' = ''> extends Channel<
ChannelType.GuildAnnouncement,
Omitted
> {
public constructor(data: Partialize<APINewsChannel, Omitted>) {
super(data);
this.optimizeData(data);
}
}
Mixin(AnnouncementChannel, [
TextChannelMixin,
ChannelParentMixin,
ChannelPermissionMixin,
ChannelPinMixin,
ChannelSlowmodeMixin,
ChannelTopicMixin,
]);

View File

@@ -0,0 +1,49 @@
import type { APIAnnouncementThreadChannel, ChannelType } from 'discord-api-types/v10';
import { Mixin } from '../Mixin.js';
import type { MixinTypes } from '../MixinTypes.d.ts';
import type { Partialize } from '../utils/types.js';
import { Channel } from './Channel.js';
import { ChannelOwnerMixin } from './mixins/ChannelOwnerMixin.js';
import { ChannelParentMixin } from './mixins/ChannelParentMixin.js';
import { ChannelPinMixin } from './mixins/ChannelPinMixin.js';
import { ChannelSlowmodeMixin } from './mixins/ChannelSlowmodeMixin.js';
import { GuildChannelMixin } from './mixins/GuildChannelMixin.js';
import { TextChannelMixin } from './mixins/TextChannelMixin.js';
import { ThreadChannelMixin } from './mixins/ThreadChannelMixin.js';
export interface AnnouncementThreadChannel<Omitted extends keyof APIAnnouncementThreadChannel | '' = ''>
extends MixinTypes<
Channel<ChannelType.AnnouncementThread>,
[
TextChannelMixin<ChannelType.AnnouncementThread>,
ChannelOwnerMixin<ChannelType.AnnouncementThread>,
ChannelParentMixin<ChannelType.AnnouncementThread>,
ChannelPinMixin<ChannelType.AnnouncementThread>,
ChannelSlowmodeMixin<ChannelType.AnnouncementThread>,
GuildChannelMixin<ChannelType.AnnouncementThread>,
ThreadChannelMixin<ChannelType.AnnouncementThread>,
]
> {}
/**
* Sample Implementation of a structure for announcement threads, usable by direct end consumers.
*/
export class AnnouncementThreadChannel<Omitted extends keyof APIAnnouncementThreadChannel | '' = ''> extends Channel<
ChannelType.AnnouncementThread,
Omitted
> {
public constructor(data: Partialize<APIAnnouncementThreadChannel, Omitted>) {
super(data);
this.optimizeData?.(data);
}
}
Mixin(AnnouncementThreadChannel, [
TextChannelMixin,
ChannelOwnerMixin,
ChannelParentMixin,
ChannelPinMixin,
ChannelSlowmodeMixin,
GuildChannelMixin,
ThreadChannelMixin,
]);

View File

@@ -0,0 +1,28 @@
import type { APIGuildCategoryChannel, ChannelType } from 'discord-api-types/v10';
import { Mixin } from '../Mixin.js';
import type { MixinTypes } from '../MixinTypes.d.ts';
import type { Partialize } from '../utils/types.js';
import { Channel } from './Channel.js';
import { ChannelPermissionMixin } from './mixins/ChannelPermissionMixin.js';
import { GuildChannelMixin } from './mixins/GuildChannelMixin.js';
export interface CategoryChannel<Omitted extends keyof APIGuildCategoryChannel | '' = ''>
extends MixinTypes<
Channel<ChannelType.GuildCategory>,
[ChannelPermissionMixin<ChannelType.GuildCategory>, GuildChannelMixin<ChannelType.GuildCategory>]
> {}
/**
* Sample Implementation of a structure for category channels, usable by direct end consumers.
*/
export class CategoryChannel<Omitted extends keyof APIGuildCategoryChannel | '' = ''> extends Channel<
ChannelType.GuildCategory,
Omitted
> {
public constructor(data: Partialize<APIGuildCategoryChannel, Omitted>) {
super(data);
this.optimizeData(data);
}
}
Mixin(CategoryChannel, [ChannelPermissionMixin, GuildChannelMixin]);

View File

@@ -0,0 +1,185 @@
import { DiscordSnowflake } from '@sapphire/snowflake';
import type { APIChannel, APIPartialChannel, ChannelType, ChannelFlags } from 'discord-api-types/v10';
import { Structure } from '../Structure.js';
import { ChannelFlagsBitField } from '../bitfields/ChannelFlagsBitField.js';
import { kData, kPatch } from '../utils/symbols.js';
import { isIdSet } from '../utils/type-guards.js';
import type { Partialize } from '../utils/types.js';
import type { ChannelPermissionMixin } from './mixins/ChannelPermissionMixin.js';
import type { ChannelWebhookMixin } from './mixins/ChannelWebhookMixin.js';
import type { DMChannelMixin } from './mixins/DMChannelMixin.js';
import type { GuildChannelMixin } from './mixins/GuildChannelMixin.js';
import type { TextChannelMixin } from './mixins/TextChannelMixin.js';
import type { ThreadChannelMixin } from './mixins/ThreadChannelMixin.js';
import type { ThreadOnlyChannelMixin } from './mixins/ThreadOnlyChannelMixin.js';
import type { VoiceChannelMixin } from './mixins/VoiceChannelMixin.js';
export type PartialChannel = Channel<ChannelType, Exclude<keyof APIChannel, keyof APIPartialChannel>>;
/**
* The data stored by a {@link Channel} structure based on its {@link (Channel:class)."type"} property.
*/
export type ChannelDataType<Type extends ChannelType | 'unknown'> = Type extends ChannelType
? Extract<APIChannel, { type: Type }>
: APIPartialChannel;
/**
* Represents any channel on Discord.
*
* @typeParam Type - Specify the type of the channel being constructed for more accurate data types
* @typeParam Omitted - Specify the properties that will not be stored in the raw data field as a union, implement via `DataTemplate`
* @remarks Although this class _can_ be instantiated directly for any channel type,
* it's intended to be subclassed with the appropriate mixins for each channel type.
*/
export class Channel<
Type extends ChannelType | 'unknown' = ChannelType,
Omitted extends keyof ChannelDataType<Type> | '' = '',
> extends Structure<ChannelDataType<Type>, Omitted> {
/**
* The template used for removing data from the raw data stored for each Channel.
*
* @remarks This template is only guaranteed to apply to channels constructed directly via `new Channel()`.
* Use the appropriate subclass template to remove data from that channel type.
*/
public static override readonly DataTemplate: Partial<APIChannel> = {};
/**
* @param data - The raw data received from the API for the channel
*/
public constructor(data: Partialize<ChannelDataType<Type>, Omitted>) {
super(data as ChannelDataType<Type>);
}
/**
* {@inheritDoc Structure.[kPatch]}
*
* @internal
*/
public override [kPatch](data: Partial<ChannelDataType<Type>>) {
return super[kPatch](data);
}
/**
* The id of the channel
*/
public get id() {
return this[kData].id;
}
/**
* The type of the channel
*/
public get type() {
// This cast can be incorrect when type is omitted and if the wrong type of channel was constructed
return this[kData].type as Type extends 'unknown' ? number : Type;
}
/**
* The name of the channel, null for DMs
*
* @privateRemarks The type of `name` can be narrowed in Guild Channels and DM channels to string and null respectively,
* respecting Omit behaviors
*/
public get name() {
return this[kData].name;
}
/**
* The flags that are applied to the channel.
*
* @privateRemarks The type of `flags` can be narrowed in Guild Channels and DMChannel to ChannelFlags, and in GroupDM channel
* to null, respecting Omit behaviors
*/
public get flags() {
const flags =
'flags' in this[kData] && typeof this[kData].flags === 'number' ? (this[kData].flags as ChannelFlags) : null;
return flags ? new ChannelFlagsBitField(flags) : null;
}
/**
* The timestamp the channel was created at
*/
public get createdTimestamp() {
return isIdSet(this.id) ? DiscordSnowflake.timestampFrom(this.id) : null;
}
/**
* The time the channel was created at
*/
public get createdAt() {
const createdTimestamp = this.createdTimestamp;
return createdTimestamp ? new Date(createdTimestamp) : null;
}
/**
* Indicates whether this channel is a thread channel
*
* @privateRemarks Overridden to `true` on `ThreadChannelMixin`
*/
public isThread(): this is ThreadChannelMixin & this {
return false;
}
/**
* Indicates whether this channel can contain messages
*
* @privateRemarks Overridden to `true` on `TextChannelMixin`
*/
public isTextBased(): this is TextChannelMixin & this {
return false;
}
/**
* Indicates whether this channel is in a guild
*
* @privateRemarks Overridden to `true` on `GuildChannelMixin`
*/
public isGuildBased(): this is GuildChannelMixin & this {
return false;
}
/**
* Indicates whether this channel is a DM or DM Group
*
* @privateRemarks Overridden to `true` on `DMChannelMixin`
*/
public isDMBased(): this is DMChannelMixin & this {
return false;
}
/**
* Indicates whether this channel has voice connection capabilities
*
* @privateRemarks Overridden to `true` on `VoiceChannelMixin`
*/
public isVoiceBased(): this is VoiceChannelMixin & this {
return false;
}
/**
* Indicates whether this channel only allows thread creation
*
* @privateRemarks Overridden to `true` on `ThreadOnlyChannelMixin`
*/
public isThreadOnly(): this is ThreadOnlyChannelMixin & this {
return false;
}
/**
* Indicates whether this channel can have permission overwrites
*
* @privateRemarks Overridden to `true` on `ChannelPermissionsMixin`
*/
public isPermissionCapable(): this is ChannelPermissionMixin & this {
return false;
}
/**
* Indicates whether this channel can have webhooks
*
* @privateRemarks Overridden to `true` on `ChannelWebhooksMixin`
*/
public isWebhookCapable(): this is ChannelWebhookMixin & this {
return false;
}
}

View File

@@ -0,0 +1,26 @@
import type { APIDMChannel, ChannelType } from 'discord-api-types/v10';
import { Mixin } from '../Mixin.js';
import type { MixinTypes } from '../MixinTypes.d.ts';
import type { Partialize } from '../utils/types.js';
import { Channel } from './Channel.js';
import { ChannelPinMixin } from './mixins/ChannelPinMixin.js';
import { DMChannelMixin } from './mixins/DMChannelMixin.js';
import { TextChannelMixin } from './mixins/TextChannelMixin.js';
export interface DMChannel<Omitted extends keyof APIDMChannel | '' = ''>
extends MixinTypes<
Channel<ChannelType.DM>,
[DMChannelMixin<ChannelType.DM>, TextChannelMixin<ChannelType.DM>, ChannelPinMixin<ChannelType.DM>]
> {}
/**
* Sample Implementation of a structure for dm channels, usable by direct end consumers.
*/
export class DMChannel<Omitted extends keyof APIDMChannel | '' = ''> extends Channel<ChannelType.DM, Omitted> {
public constructor(data: Partialize<APIDMChannel, Omitted>) {
super(data);
this.optimizeData(data);
}
}
Mixin(DMChannel, [DMChannelMixin, TextChannelMixin, ChannelPinMixin]);

View File

@@ -0,0 +1,44 @@
import type { APIGuildForumChannel, ChannelType } from 'discord-api-types/v10';
import { Mixin } from '../Mixin.js';
import type { MixinTypes } from '../MixinTypes.d.ts';
import { kData } from '../utils/symbols.js';
import type { Partialize } from '../utils/types.js';
import { Channel } from './Channel.js';
import { ChannelParentMixin } from './mixins/ChannelParentMixin.js';
import { ChannelPermissionMixin } from './mixins/ChannelPermissionMixin.js';
import { ChannelTopicMixin } from './mixins/ChannelTopicMixin.js';
import { ThreadOnlyChannelMixin } from './mixins/ThreadOnlyChannelMixin.js';
export interface ForumChannel<Omitted extends keyof APIGuildForumChannel | '' = ''>
extends MixinTypes<
Channel<ChannelType.GuildForum>,
[
ChannelParentMixin<ChannelType.GuildForum>,
ChannelPermissionMixin<ChannelType.GuildForum>,
ChannelTopicMixin<ChannelType.GuildForum>,
ThreadOnlyChannelMixin<ChannelType.GuildForum>,
]
> {}
/**
* Sample Implementation of a structure for forum channels, usable by direct end consumers.
*/
export class ForumChannel<Omitted extends keyof APIGuildForumChannel | '' = ''> extends Channel<
ChannelType.GuildForum,
Omitted
> {
public constructor(data: Partialize<APIGuildForumChannel, Omitted>) {
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, [ChannelParentMixin, ChannelPermissionMixin, ChannelTopicMixin, ThreadOnlyChannelMixin]);

View File

@@ -0,0 +1,57 @@
import type { APIGuildForumTag } from 'discord-api-types/v10';
import { Structure } from '../Structure.js';
import { kData } from '../utils/symbols.js';
import type { Partialize } from '../utils/types.js';
/**
* Represents metadata of a thread channel on Discord.
*
* @typeParam Omitted - Specify the properties that will not be stored in the raw data field as a union, implement via `DataTemplate`
*/
export class ForumTag<Omitted extends keyof APIGuildForumTag | '' = ''> extends Structure<APIGuildForumTag, Omitted> {
public constructor(data: Partialize<APIGuildForumTag, Omitted>) {
super(data);
}
/**
* The id of the tag.
*/
public get id() {
return this[kData].id;
}
/**
* The name of the tag.
*/
public get name() {
return this[kData].name;
}
/**
* Whether this tag can only be added to or removed from threads by a member with the {@link discord-api-types/v10#(PermissionFlagsBits:variable) | ManageThreads} permission.
*/
public get moderated() {
return this[kData].moderated;
}
/**
* The id of a guild's custom emoji.
*/
public get emojiId() {
return this[kData].emoji_id;
}
/**
* The unicode character of the emoji.
*/
public get emojiName() {
return this[kData].emoji_name;
}
/**
* The textual representation of this tag's emoji. Either a unicode character or a guild emoji mention.
*/
public get emoji() {
return this.emojiName ?? `<:_:${this.emojiId}>`;
}
}

View File

@@ -0,0 +1,35 @@
import type { APIGroupDMChannel, ChannelType } from 'discord-api-types/v10';
import { Mixin } from '../Mixin.js';
import type { MixinTypes } from '../MixinTypes.d.ts';
import type { Partialize } from '../utils/types.js';
import { Channel } from './Channel.js';
import { ChannelOwnerMixin } from './mixins/ChannelOwnerMixin.js';
import { DMChannelMixin } from './mixins/DMChannelMixin.js';
import { GroupDMMixin } from './mixins/GroupDMMixin.js';
import { TextChannelMixin } from './mixins/TextChannelMixin.js';
export interface GroupDMChannel<Omitted extends keyof APIGroupDMChannel | '' = ''>
extends MixinTypes<
Channel<ChannelType.GroupDM>,
[
DMChannelMixin<ChannelType.GroupDM>,
TextChannelMixin<ChannelType.GroupDM>,
ChannelOwnerMixin<ChannelType.GroupDM>,
GroupDMMixin,
]
> {}
/**
* Sample Implementation of a structure for group dm channels, usable by direct end consumers.
*/
export class GroupDMChannel<Omitted extends keyof APIGroupDMChannel | '' = ''> extends Channel<
ChannelType.GroupDM,
Omitted
> {
public constructor(data: Partialize<APIGroupDMChannel, Omitted>) {
super(data);
this.optimizeData(data);
}
}
Mixin(GroupDMChannel, [DMChannelMixin, TextChannelMixin, ChannelOwnerMixin, GroupDMMixin]);

View File

@@ -0,0 +1,35 @@
import type { APIGuildMediaChannel, ChannelType } from 'discord-api-types/v10';
import { Mixin } from '../Mixin.js';
import type { MixinTypes } from '../MixinTypes.d.ts';
import type { Partialize } from '../utils/types.js';
import { Channel } from './Channel.js';
import { ChannelParentMixin } from './mixins/ChannelParentMixin.js';
import { ChannelPermissionMixin } from './mixins/ChannelPermissionMixin.js';
import { ChannelTopicMixin } from './mixins/ChannelTopicMixin.js';
import { ThreadOnlyChannelMixin } from './mixins/ThreadOnlyChannelMixin.js';
export interface MediaChannel<Omitted extends keyof APIGuildMediaChannel | '' = ''>
extends MixinTypes<
Channel<ChannelType.GuildMedia>,
[
ChannelParentMixin<ChannelType.GuildMedia>,
ChannelPermissionMixin<ChannelType.GuildMedia>,
ChannelTopicMixin<ChannelType.GuildMedia>,
ThreadOnlyChannelMixin<ChannelType.GuildMedia>,
]
> {}
/**
* Sample Implementation of a structure for media channels, usable by direct end consumers.
*/
export class MediaChannel<Omitted extends keyof APIGuildMediaChannel | '' = ''> extends Channel<
ChannelType.GuildMedia,
Omitted
> {
public constructor(data: Partialize<APIGuildMediaChannel, Omitted>) {
super(data);
this.optimizeData(data);
}
}
Mixin(MediaChannel, [ChannelParentMixin, ChannelPermissionMixin, ChannelTopicMixin, ThreadOnlyChannelMixin]);

View File

@@ -0,0 +1,94 @@
import type { APIOverwrite } from 'discord-api-types/v10';
import { Structure } from '../Structure.js';
import { PermissionsBitField } from '../bitfields/PermissionsBitField.js';
import { kAllow, kData, kDeny } from '../utils/symbols.js';
import type { Partialize } from '../utils/types.js';
/**
* Represents metadata of a thread channel on Discord.
*
* @typeParam Omitted - Specify the properties that will not be stored in the raw data field as a union, implement via `DataTemplate`
*/
export class PermissionOverwrite<Omitted extends keyof APIOverwrite | '' = 'allow' | 'deny'> extends Structure<
APIOverwrite,
Omitted
> {
protected [kAllow]: bigint | null = null;
protected [kDeny]: bigint | null = null;
public constructor(data: Partialize<APIOverwrite, Omitted>) {
super(data);
this.optimizeData(data);
}
/**
* The template used for removing data from the raw data stored for each ThreadMetadata
*
* @remarks This template has defaults, if you want to remove additional data and keep the defaults,
* use `Object.defineProperties`. To override the defaults, set this value directly.
*/
public static override readonly DataTemplate: Partial<APIOverwrite> = {
set allow(_: string) {},
set deny(_: string) {},
};
/**
* {@inheritDoc Structure.optimizeData}
*/
protected override optimizeData(data: Partial<APIOverwrite>) {
if (data.allow) {
this[kAllow] = BigInt(data.allow);
}
if (data.deny) {
this[kDeny] = BigInt(data.deny);
}
}
/**
* The permission bit set allowed by this overwrite.
*/
public get allow() {
const allow = this[kAllow];
return typeof allow === 'bigint' ? new PermissionsBitField(allow) : null;
}
/**
* The permission bit set denied by this overwrite.
*/
public get deny() {
const deny = this[kDeny];
return typeof deny === 'bigint' ? new PermissionsBitField(deny) : null;
}
/**
* The role or user id for this overwrite.
*/
public get id() {
return this[kData].id;
}
/**
* The type of this overwrite.
*/
public get type() {
return this[kData].type;
}
/**
* {@inheritDoc Structure.toJSON}
*/
public override toJSON() {
const clone = super.toJSON();
if (this[kAllow]) {
clone.allow = this[kAllow].toString();
}
if (this[kDeny]) {
clone.deny = this[kDeny].toString();
}
return clone;
}
}

View File

@@ -0,0 +1,46 @@
import type { APIPrivateThreadChannel, ChannelType } from 'discord-api-types/v10';
import { Mixin } from '../Mixin.js';
import type { MixinTypes } from '../MixinTypes.d.ts';
import type { Partialize } from '../utils/types.js';
import { Channel } from './Channel.js';
import { ChannelOwnerMixin } from './mixins/ChannelOwnerMixin.js';
import { ChannelParentMixin } from './mixins/ChannelParentMixin.js';
import { ChannelPinMixin } from './mixins/ChannelPinMixin.js';
import { ChannelSlowmodeMixin } from './mixins/ChannelSlowmodeMixin.js';
import { TextChannelMixin } from './mixins/TextChannelMixin.js';
import { ThreadChannelMixin } from './mixins/ThreadChannelMixin.js';
export interface PrivateThreadChannel<Omitted extends keyof APIPrivateThreadChannel | '' = ''>
extends MixinTypes<
Channel<ChannelType.PrivateThread>,
[
TextChannelMixin<ChannelType.PrivateThread>,
ChannelOwnerMixin<ChannelType.PrivateThread>,
ChannelParentMixin<ChannelType.PrivateThread>,
ChannelPinMixin<ChannelType.PrivateThread>,
ChannelSlowmodeMixin<ChannelType.PrivateThread>,
ThreadChannelMixin<ChannelType.PrivateThread>,
]
> {}
/**
* Sample Implementation of a structure for private thread channels, usable by direct end consumers.
*/
export class PrivateThreadChannel<Omitted extends keyof APIPrivateThreadChannel | '' = ''> extends Channel<
ChannelType.PrivateThread,
Omitted
> {
public constructor(data: Partialize<APIPrivateThreadChannel, Omitted>) {
super(data);
this.optimizeData(data);
}
}
Mixin(PrivateThreadChannel, [
TextChannelMixin,
ChannelOwnerMixin,
ChannelParentMixin,
ChannelPinMixin,
ChannelSlowmodeMixin,
ThreadChannelMixin,
]);

View File

@@ -0,0 +1,49 @@
import type { APIPublicThreadChannel, ChannelType } from 'discord-api-types/v10';
import { Mixin } from '../Mixin.js';
import type { MixinTypes } from '../MixinTypes.d.ts';
import type { Partialize } from '../utils/types.js';
import { Channel } from './Channel.js';
import { AppliedTagsMixin } from './mixins/AppliedTagsMixin.js';
import { ChannelOwnerMixin } from './mixins/ChannelOwnerMixin.js';
import { ChannelParentMixin } from './mixins/ChannelParentMixin.js';
import { ChannelPinMixin } from './mixins/ChannelPinMixin.js';
import { ChannelSlowmodeMixin } from './mixins/ChannelSlowmodeMixin.js';
import { TextChannelMixin } from './mixins/TextChannelMixin.js';
import { ThreadChannelMixin } from './mixins/ThreadChannelMixin.js';
export interface PublicThreadChannel<Omitted extends keyof APIPublicThreadChannel | '' = ''>
extends MixinTypes<
Channel<ChannelType.PublicThread>,
[
TextChannelMixin<ChannelType.PublicThread>,
ChannelOwnerMixin<ChannelType.PublicThread>,
ChannelParentMixin<ChannelType.PublicThread>,
ChannelPinMixin<ChannelType.PublicThread>,
ChannelSlowmodeMixin<ChannelType.PublicThread>,
ThreadChannelMixin<ChannelType.PublicThread>,
AppliedTagsMixin,
]
> {}
/**
* Sample Implementation of a structure for public thread channels, usable by direct end consumers.
*/
export class PublicThreadChannel<Omitted extends keyof APIPublicThreadChannel | '' = ''> extends Channel<
ChannelType.PublicThread,
Omitted
> {
public constructor(data: Partialize<APIPublicThreadChannel, Omitted>) {
super(data);
this.optimizeData(data);
}
}
Mixin(PublicThreadChannel, [
TextChannelMixin,
ChannelOwnerMixin,
ChannelParentMixin,
ChannelPinMixin,
ChannelSlowmodeMixin,
ThreadChannelMixin,
AppliedTagsMixin,
]);

View File

@@ -0,0 +1,40 @@
import type { APIGuildStageVoiceChannel, ChannelType } from 'discord-api-types/v10';
import { Mixin } from '../Mixin.js';
import type { MixinTypes } from '../MixinTypes.d.ts';
import type { Partialize } from '../utils/types.js';
import { Channel } from './Channel.js';
import { ChannelParentMixin } from './mixins/ChannelParentMixin.js';
import { ChannelPermissionMixin } from './mixins/ChannelPermissionMixin.js';
import { ChannelSlowmodeMixin } from './mixins/ChannelSlowmodeMixin.js';
import { ChannelWebhookMixin } from './mixins/ChannelWebhookMixin.js';
import { VoiceChannelMixin } from './mixins/VoiceChannelMixin.js';
export interface StageChannel<Omitted extends keyof APIGuildStageVoiceChannel | '' = ''>
extends MixinTypes<
Channel<ChannelType.GuildStageVoice>,
[
ChannelParentMixin<ChannelType.GuildStageVoice>,
ChannelPermissionMixin<ChannelType.GuildStageVoice>,
ChannelSlowmodeMixin<ChannelType.GuildStageVoice>,
ChannelWebhookMixin<ChannelType.GuildStageVoice>,
VoiceChannelMixin<ChannelType.GuildStageVoice>,
]
> {}
export class StageChannel<Omitted extends keyof APIGuildStageVoiceChannel | '' = ''> extends Channel<
ChannelType.GuildStageVoice,
Omitted
> {
public constructor(data: Partialize<APIGuildStageVoiceChannel, Omitted>) {
super(data);
this.optimizeData(data);
}
}
Mixin(StageChannel, [
ChannelParentMixin,
ChannelPermissionMixin,
ChannelSlowmodeMixin,
ChannelWebhookMixin,
VoiceChannelMixin,
]);

View File

@@ -0,0 +1,43 @@
import type { APITextChannel, ChannelType } from 'discord-api-types/v10';
import { Mixin } from '../Mixin.js';
import type { MixinTypes } from '../MixinTypes.d.ts';
import type { Partialize } from '../utils/types.js';
import { Channel } from './Channel.js';
import { ChannelParentMixin } from './mixins/ChannelParentMixin.js';
import { ChannelPermissionMixin } from './mixins/ChannelPermissionMixin.js';
import { ChannelPinMixin } from './mixins/ChannelPinMixin.js';
import { ChannelSlowmodeMixin } from './mixins/ChannelSlowmodeMixin.js';
import { ChannelTopicMixin } from './mixins/ChannelTopicMixin.js';
import { TextChannelMixin } from './mixins/TextChannelMixin.js';
export interface TextChannel<Omitted extends keyof APITextChannel | '' = ''>
extends MixinTypes<
Channel<ChannelType.GuildText>,
[
TextChannelMixin<ChannelType.GuildText>,
ChannelParentMixin<ChannelType.GuildText>,
ChannelPermissionMixin<ChannelType.GuildText>,
ChannelPinMixin<ChannelType.GuildText>,
ChannelSlowmodeMixin<ChannelType.GuildText>,
ChannelTopicMixin<ChannelType.GuildText>,
]
> {}
export class TextChannel<Omitted extends keyof APITextChannel | '' = ''> extends Channel<
ChannelType.GuildText,
Omitted
> {
public constructor(data: Partialize<APITextChannel, Omitted>) {
super(data);
this.optimizeData(data);
}
}
Mixin(TextChannel, [
TextChannelMixin,
ChannelParentMixin,
ChannelPermissionMixin,
ChannelPinMixin,
ChannelSlowmodeMixin,
ChannelTopicMixin,
]);

View File

@@ -0,0 +1,120 @@
import type { APIThreadMetadata } from 'discord-api-types/v10';
import { Structure } from '../Structure.js';
import { kArchiveTimestamp, kCreatedTimestamp, kData } from '../utils/symbols.js';
import type { Partialize } from '../utils/types.js';
/**
* Represents metadata of a thread channel on Discord.
*
* @typeParam Omitted - Specify the properties that will not be stored in the raw data field as a union, implement via `DataTemplate`
*/
export class ThreadMetadata<
Omitted extends keyof APIThreadMetadata | '' = 'archive_timestamp' | 'create_timestamp',
> extends Structure<APIThreadMetadata, Omitted> {
protected [kArchiveTimestamp]: number | null = null;
protected [kCreatedTimestamp]: number | null = null;
public constructor(data: Partialize<APIThreadMetadata, Omitted>) {
super(data);
this.optimizeData(data);
}
/**
* The template used for removing data from the raw data stored for each ThreadMetadata
*
* @remarks This template has defaults, if you want to remove additional data and keep the defaults,
* use `Object.defineProperties`. To override the defaults, set this value directly.
*/
public static override readonly DataTemplate: Partial<APIThreadMetadata> = {
set create_timestamp(_: string) {},
set archive_timestamp(_: string) {},
};
/**
* {@inheritDoc Structure.optimizeData}
*/
protected override optimizeData(data: Partial<APIThreadMetadata>) {
if (data.create_timestamp) {
this[kCreatedTimestamp] = Date.parse(data.create_timestamp);
}
if (data.archive_timestamp) {
this[kArchiveTimestamp] = Date.parse(data.archive_timestamp);
}
}
/**
* Whether the thread is archived.
*/
public get archived() {
return this[kData].archived;
}
/**
* The timestamp when the thread's archive status was last changed, used for calculating recent activity.
*/
public get archivedTimestamp() {
return this[kArchiveTimestamp];
}
/**
* The timestamp when the thread was created; only populated for threads created after 2022-01-09.
*/
public get createdTimestamp() {
return this[kCreatedTimestamp];
}
/**
* The thread will stop showing in the channel list after auto_archive_duration minutes of inactivity,
*/
public get autoArchiveDuration() {
return this[kData].auto_archive_duration;
}
/**
* Whether non-moderators can add other non-moderators to a thread; only available on private threads.
*/
public get invitable() {
return this[kData].invitable;
}
/**
* Whether the thread is locked; when a thread is locked, only users with {@link discord-api-types/v10#(PermissionFlagsBits:variable) | ManageThreads} can unarchive it.
*/
public get locked() {
return this[kData].locked;
}
/**
* The time the thread was archived at
*/
public get archivedAt() {
const archivedTimestamp = this.archivedTimestamp;
return archivedTimestamp ? new Date(archivedTimestamp) : null;
}
/**
* The time the thread was created at
*/
public get createdAt() {
const createdTimestamp = this.createdTimestamp;
return createdTimestamp ? new Date(createdTimestamp) : null;
}
/**
* {@inheritDoc Structure.toJSON}
*/
public override toJSON() {
const data = super.toJSON();
if (this[kArchiveTimestamp]) {
data.archive_timestamp = new Date(this[kArchiveTimestamp]).toISOString();
}
if (this[kCreatedTimestamp]) {
data.create_timestamp = new Date(this[kCreatedTimestamp]).toISOString();
}
return data;
}
}

View File

@@ -0,0 +1,40 @@
import type { APIGuildVoiceChannel, ChannelType } from 'discord-api-types/v10';
import { Mixin } from '../Mixin.js';
import type { MixinTypes } from '../MixinTypes.d.ts';
import type { Partialize } from '../utils/types.js';
import { Channel } from './Channel.js';
import { ChannelParentMixin } from './mixins/ChannelParentMixin.js';
import { ChannelPermissionMixin } from './mixins/ChannelPermissionMixin.js';
import { ChannelSlowmodeMixin } from './mixins/ChannelSlowmodeMixin.js';
import { ChannelWebhookMixin } from './mixins/ChannelWebhookMixin.js';
import { VoiceChannelMixin } from './mixins/VoiceChannelMixin.js';
export interface VoiceChannel<Omitted extends keyof APIGuildVoiceChannel | '' = ''>
extends MixinTypes<
Channel<ChannelType.GuildVoice>,
[
ChannelParentMixin<ChannelType.GuildVoice>,
ChannelPermissionMixin<ChannelType.GuildVoice>,
ChannelSlowmodeMixin<ChannelType.GuildVoice>,
ChannelWebhookMixin<ChannelType.GuildVoice>,
VoiceChannelMixin<ChannelType.GuildVoice>,
]
> {}
export class VoiceChannel<Omitted extends keyof APIGuildVoiceChannel | '' = ''> extends Channel<
ChannelType.GuildVoice,
Omitted
> {
public constructor(data: Partialize<APIGuildVoiceChannel, Omitted>) {
super(data);
this.optimizeData(data);
}
}
Mixin(VoiceChannel, [
ChannelParentMixin,
ChannelPermissionMixin,
ChannelSlowmodeMixin,
ChannelWebhookMixin,
VoiceChannelMixin,
]);

View File

@@ -0,0 +1,21 @@
export * from './mixins/index.js';
export * from './ForumTag.js';
export * from './PermissionOverwrite.js';
export * from './ThreadMetadata.js';
export * from './Channel.js';
export * from './AnnouncementChannel.js';
export * from './AnnouncementThreadChannel.js';
export * from './CategoryChannel.js';
// export * from './DirectoryChannel.js';
export * from './DMChannel.js';
export * from './ForumChannel.js';
export * from './GroupDMChannel.js';
export * from './MediaChannel.js';
export * from './PrivateThreadChannel.js';
export * from './PublicThreadChannel.js';
export * from './StageChannel.js';
export * from './TextChannel.js';
export * from './VoiceChannel.js';

View File

@@ -0,0 +1,14 @@
import type { ChannelType } from 'discord-api-types/v10';
import { kData } from '../../utils/symbols.js';
import type { Channel } from '../Channel.js';
export interface AppliedTagsMixin extends Channel<ChannelType.PublicThread> {}
export class AppliedTagsMixin {
/**
* The ids of the set of tags that have been applied to a thread in a {@link (ForumChannel:class)} or a {@link (MediaChannel:class)}.
*/
public get appliedTags(): readonly string[] | null {
return Array.isArray(this[kData].applied_tags) ? this[kData].applied_tags : null;
}
}

View File

@@ -0,0 +1,14 @@
import type { ChannelType, ThreadChannelType } from 'discord-api-types/v10';
import { kData } from '../../utils/symbols.js';
import type { Channel } from '../Channel.js';
export interface ChannelOwnerMixin<Type extends ChannelType.GroupDM | ThreadChannelType> extends Channel<Type> {}
export class ChannelOwnerMixin<Type extends ChannelType.GroupDM | ThreadChannelType> {
/**
* The id of the creator of the group DM or thread
*/
public get ownerId() {
return this[kData].owner_id;
}
}

View File

@@ -0,0 +1,21 @@
import type { ChannelType, GuildChannelType } from 'discord-api-types/v10';
import { kData } from '../../utils/symbols.js';
import { GuildChannelMixin } from './GuildChannelMixin.js';
export class ChannelParentMixin<
Type extends Exclude<GuildChannelType, ChannelType.GuildCategory | ChannelType.GuildDirectory>,
> extends GuildChannelMixin<Type> {
/**
* The id of the parent category for a channel (each parent category can contain up to 50 channels) or id of the parent channel for a thread
*/
public get parentId() {
return this[kData].parent_id;
}
/**
* Whether the channel is nsfw
*/
public get nsfw() {
return this[kData].nsfw;
}
}

View File

@@ -0,0 +1,34 @@
import type { ChannelType, GuildChannelType, ThreadChannelType } from 'discord-api-types/v10';
import { kData } from '../../utils/symbols.js';
import type { Channel } from '../Channel.js';
export interface ChannelPermissionMixin<
Type extends Exclude<GuildChannelType, ChannelType.GuildDirectory | ThreadChannelType> = Exclude<
GuildChannelType,
ChannelType.GuildDirectory | ThreadChannelType
>,
> extends Channel<Type> {}
/**
* @remarks has an array of sub-structures {@link PermissionOverwrite} that extending mixins should add to their DataTemplate and _optimizeData
*/
export class ChannelPermissionMixin<
Type extends Exclude<GuildChannelType, ChannelType.GuildDirectory | ThreadChannelType> = Exclude<
GuildChannelType,
ChannelType.GuildDirectory | ThreadChannelType
>,
> {
/**
* The sorting position of the channel
*/
public get position() {
return this[kData].position;
}
/**
* Indicates whether this channel can have permission overwrites
*/
public isPermissionCapable(): this is ChannelPermissionMixin & this {
return true;
}
}

View File

@@ -0,0 +1,62 @@
import type { ChannelType, ThreadChannelType } from 'discord-api-types/v10';
import { kLastPinTimestamp, kMixinConstruct, kMixinToJSON } from '../../utils/symbols.js';
import type { Channel, ChannelDataType } from '../Channel.js';
export interface ChannelPinMixin<
Type extends ChannelType.DM | ChannelType.GuildAnnouncement | ChannelType.GuildText | ThreadChannelType,
> extends Channel<Type> {}
export class ChannelPinMixin<
Type extends ChannelType.DM | ChannelType.GuildAnnouncement | ChannelType.GuildText | ThreadChannelType,
> {
/**
* The timestamp of when the last pin in the channel happened
*/
declare protected [kLastPinTimestamp]: number | null;
/**
* The template used for removing data from the raw data stored for each Channel.
*/
public static readonly DataTemplate: Partial<
ChannelDataType<ChannelType.DM | ChannelType.GuildAnnouncement | ChannelType.GuildText | ThreadChannelType>
> = {
set last_pin_timestamp(_: string) {},
};
public [kMixinConstruct]() {
this[kLastPinTimestamp] ??= null;
}
/**
* {@inheritDoc Structure.optimizeData}
*/
protected optimizeData(data: Partial<ChannelDataType<Type>>) {
if (data.last_pin_timestamp) {
this[kLastPinTimestamp] = Date.parse(data.last_pin_timestamp);
}
}
/**
* The timestamp of when the last pin in the channel happened.
*/
public get lastPinTimestamp() {
return this[kLastPinTimestamp];
}
/**
* The Date of when the last pin in the channel happened
*/
public get lastPinAt() {
const lastPinTimestamp = this.lastPinTimestamp;
return lastPinTimestamp ? new Date(lastPinTimestamp) : null;
}
/**
* Adds data from optimized properties omitted from [kData].
*
* @param data - the result of {@link (Structure:class).toJSON}
*/
protected [kMixinToJSON](data: Partial<ChannelDataType<Type>>) {
data.last_pin_timestamp = this[kLastPinTimestamp] ? new Date(this[kLastPinTimestamp]).toISOString() : null;
}
}

View File

@@ -0,0 +1,12 @@
import type { GuildTextChannelType } from 'discord-api-types/v10';
import { kData } from '../../utils/symbols.js';
import { TextChannelMixin } from './TextChannelMixin.js';
export class ChannelSlowmodeMixin<Type extends GuildTextChannelType> extends TextChannelMixin<Type> {
/**
* The rate limit per user (slowmode) of this channel.
*/
public get rateLimitPerUser() {
return this[kData].rate_limit_per_user;
}
}

View File

@@ -0,0 +1,33 @@
import type { ChannelType } from 'discord-api-types/v10';
import { kData } from '../../utils/symbols.js';
import type { Channel } from '../Channel.js';
import { ChannelWebhookMixin } from './ChannelWebhookMixin.js';
export interface ChannelTopicMixin<
Type extends ChannelType.GuildAnnouncement | ChannelType.GuildForum | ChannelType.GuildMedia | ChannelType.GuildText,
> extends Channel<Type> {}
export class ChannelTopicMixin<
Type extends ChannelType.GuildAnnouncement | ChannelType.GuildForum | ChannelType.GuildMedia | ChannelType.GuildText,
> extends ChannelWebhookMixin<Type> {
/**
* The topic of this channel.
*/
public get topic() {
return this[kData].topic;
}
/**
* The duration after which new threads get archived by default on this channel.
*/
public get defaultAutoArchiveDuration() {
return this[kData].default_auto_archive_duration;
}
/**
* The default value for rate limit per user (slowmode) on new threads in this channel.
*/
public get defaultThreadRateLimitPerUser() {
return this[kData].default_thread_rate_limit_per_user;
}
}

View File

@@ -0,0 +1,23 @@
import type { ChannelType, GuildTextChannelType, ThreadChannelType } from 'discord-api-types/v10';
import type { Channel } from '../Channel.js';
export interface ChannelWebhookMixin<
Type extends ChannelType.GuildForum | ChannelType.GuildMedia | Exclude<GuildTextChannelType, ThreadChannelType> =
| ChannelType.GuildForum
| ChannelType.GuildMedia
| Exclude<GuildTextChannelType, ThreadChannelType>,
> extends Channel<Type> {}
export class ChannelWebhookMixin<
Type extends ChannelType.GuildForum | ChannelType.GuildMedia | Exclude<GuildTextChannelType, ThreadChannelType> =
| ChannelType.GuildForum
| ChannelType.GuildMedia
| Exclude<GuildTextChannelType, ThreadChannelType>,
> {
/**
* Indicates whether this channel can have webhooks
*/
public isWebhookCapable(): this is ChannelWebhookMixin & this {
return true;
}
}

View File

@@ -0,0 +1,27 @@
import { channelLink } from '@discordjs/formatters';
import type { ChannelType } from 'discord-api-types/v10';
import type { User } from '../../users/User.js';
import type { Channel } from '../Channel.js';
export interface DMChannelMixin<
Type extends ChannelType.DM | ChannelType.GroupDM = ChannelType.DM | ChannelType.GroupDM,
> extends Channel<Type> {}
/**
* @remarks has recipients, an array of sub-structures {@link User} that extending mixins should add to their DataTemplate and _optimizeData
*/
export class DMChannelMixin<Type extends ChannelType.DM | ChannelType.GroupDM = ChannelType.DM | ChannelType.GroupDM> {
/**
* The URL to this channel.
*/
public get url() {
return channelLink(this.id);
}
/**
* Indicates whether this channel is a DM or DM Group
*/
public isDMBased(): this is DMChannelMixin & this {
return true;
}
}

View File

@@ -0,0 +1,28 @@
import type { ChannelType } from 'discord-api-types/v10';
import { kData } from '../../utils/symbols.js';
import type { Channel } from '../Channel.js';
export interface GroupDMMixin extends Channel<ChannelType.GroupDM> {}
export class GroupDMMixin {
/**
* The icon hash of the group DM.
*/
public get icon() {
return this[kData].icon;
}
/**
* Whether the channel is managed by an application via the `gdm.join` OAuth2 scope.
*/
public get managed() {
return this[kData].managed;
}
/**
* The application id of the group DM creator if it is bot-created.
*/
public get applicationId() {
return this[kData].application_id;
}
}

View File

@@ -0,0 +1,40 @@
import { channelLink } from '@discordjs/formatters';
import type { GuildChannelType } from 'discord-api-types/v10';
import { ChannelFlagsBitField } from '../../bitfields/ChannelFlagsBitField.js';
import { kData } from '../../utils/symbols.js';
import type { Channel } from '../Channel.js';
export interface GuildChannelMixin<Type extends GuildChannelType = GuildChannelType> extends Channel<Type> {}
export class GuildChannelMixin<Type extends GuildChannelType = GuildChannelType> {
/**
* The flags that are applied to the channel.
*
* @privateRemarks The type of `flags` can be narrowed in Guild Channels and DMChannel to ChannelFlags, and in GroupDM channel
* to null, respecting Omit behaviors
*/
public get flags() {
return this[kData].flags ? new ChannelFlagsBitField(this[kData].flags) : null;
}
/**
* THe id of the guild this channel is in.
*/
public get guildId() {
return this[kData].guild_id!;
}
/**
* The URL to this channel.
*/
public get url() {
return channelLink(this.id, this.guildId);
}
/**
* Indicates whether this channel is in a guild
*/
public isGuildBased(): this is GuildChannelMixin & this {
return true;
}
}

View File

@@ -0,0 +1,21 @@
import type { TextChannelType } from 'discord-api-types/v10';
import { kData } from '../../utils/symbols.js';
import type { Channel } from '../Channel.js';
export interface TextChannelMixin<Type extends TextChannelType = TextChannelType> extends Channel<Type> {}
export class TextChannelMixin<Type extends TextChannelType = TextChannelType> {
/**
* The id of the last message sent in this channel.
*/
public get lastMessageId() {
return this[kData].last_message_id;
}
/**
* Indicates whether this channel can contain messages
*/
public isTextBased(): this is TextChannelMixin & this {
return true;
}
}

View File

@@ -0,0 +1,39 @@
import type { ThreadChannelType } from 'discord-api-types/v10';
import { kData } from '../../utils/symbols.js';
import type { Channel } from '../Channel.js';
export interface ThreadChannelMixin<Type extends ThreadChannelType = ThreadChannelType> extends Channel<Type> {}
/**
* @remarks has a sub-structure {@link ThreadMetadata} that extending mixins should add to their DataTemplate and _optimizeData
*/
export class ThreadChannelMixin<Type extends ThreadChannelType = ThreadChannelType> {
/**
* The approximate count of users in a thread, stops counting at 50
*/
public get memberCount() {
return this[kData].member_count;
}
/**
* The number of messages (not including the initial message or deleted messages) in a thread.
*/
public get messageCount() {
return this[kData].message_count;
}
/**
* The number of messages ever sent in a thread, it's similar to message_count on message creation,
* but will not decrement the number when a message is deleted.
*/
public get totalMessageSent() {
return this[kData].total_message_sent;
}
/**
* Indicates whether this channel is a thread channel
*/
public isThread(): this is ThreadChannelMixin & this {
return true;
}
}

View File

@@ -0,0 +1,37 @@
import type { ChannelType } from 'discord-api-types/v10';
import { kData } from '../../utils/symbols.js';
import type { Channel } from '../Channel.js';
export interface ThreadOnlyChannelMixin<
Type extends ChannelType.GuildForum | ChannelType.GuildMedia = ChannelType.GuildForum | ChannelType.GuildMedia,
> extends Channel<Type> {}
/**
* @remarks has an array of sub-structures {@link ForumTag} that extending mixins should add to their DataTemplate and _optimizeData
*/
export class ThreadOnlyChannelMixin<
Type extends ChannelType.GuildForum | ChannelType.GuildMedia = ChannelType.GuildForum | ChannelType.GuildMedia,
> {
/**
* The emoji to show in the add reaction button on a thread in this channel.
*/
public get defaultReactionEmoji() {
return this[kData].default_reaction_emoji;
}
/**
* The default sort order type used to order posts in this channel.
*
* @defaultValue `null` indicates a preferred sort order hasn't been set.
*/
public get defaultSortOrder() {
return this[kData].default_sort_order!;
}
/**
* Indicates whether this channel only allows thread creation
*/
public isThreadOnly(): this is ThreadOnlyChannelMixin & this {
return true;
}
}

View File

@@ -0,0 +1,51 @@
import type { ChannelType } from 'discord-api-types/v10';
import { kData } from '../../utils/symbols.js';
import type { Channel } from '../Channel.js';
import { TextChannelMixin } from './TextChannelMixin.js';
export interface VoiceChannelMixin<
Type extends ChannelType.GuildStageVoice | ChannelType.GuildVoice =
| ChannelType.GuildStageVoice
| ChannelType.GuildVoice,
> extends Channel<Type> {}
export class VoiceChannelMixin<
Type extends ChannelType.GuildStageVoice | ChannelType.GuildVoice =
| ChannelType.GuildStageVoice
| ChannelType.GuildVoice,
> extends TextChannelMixin<Type> {
/**
* The bitrate (in bits) of the voice channel.
*/
public get bitrate() {
return this[kData].bitrate!;
}
/**
* The voice region id for this channel, automatic when set to null.
*/
public get rtcRegion() {
return this[kData].rtc_region!;
}
/**
* The camera video quality mode of the voice channel, {@link discord-api-types/v10#(VideoQualityMode:enum) | Auto} when not present.
*/
public get videoQualityMode() {
return this[kData].video_quality_mode!;
}
/**
* The user limit of the voice channel.
*/
public get userLimit() {
return this[kData].user_limit!;
}
/**
* Indicates whether this channel has voice connection capabilities
*/
public override isVoiceBased(): this is VoiceChannelMixin & this {
return true;
}
}

View File

@@ -0,0 +1,15 @@
export * from './AppliedTagsMixin.js';
export * from './ChannelOwnerMixin.js';
export * from './ChannelParentMixin.js';
export * from './ChannelPermissionMixin.js';
export * from './ChannelPinMixin.js';
export * from './ChannelSlowmodeMixin.js';
export * from './ChannelTopicMixin.js';
export * from './ChannelWebhookMixin.js';
export * from './DMChannelMixin.js';
export * from './GroupDMMixin.js';
export * from './GuildChannelMixin.js';
export * from './TextChannelMixin.js';
export * from './ThreadChannelMixin.js';
export * from './ThreadOnlyChannelMixin.js';
export * from './VoiceChannelMixin.js';

View File

@@ -0,0 +1,9 @@
export * from './bitfields/index.js';
export * from './channels/index.js';
export * from './invites/index.js';
export * from './users/index.js';
export * from './Structure.js';
export * from './Mixin.js';
export * from './utils/optimization.js';
export type * from './utils/types.js';
export type * from './MixinTypes.d.ts';

View File

@@ -0,0 +1,220 @@
import { type APIInvite, type APIExtendedInvite, RouteBases } from 'discord-api-types/v10';
import { Structure } from '../Structure.js';
import { kData, kExpiresTimestamp, kCreatedTimestamp, kPatch } from '../utils/symbols.js';
import type { Partialize } from '../utils/types.js';
export interface APIActualInvite extends APIInvite, Partial<Omit<APIExtendedInvite, keyof APIInvite>> {}
/**
* Represents an invitation to a Discord channel
*
* @typeParam Omitted - Specify the properties that will not be stored in the raw data field as a union, implement via `DataTemplate`
*/
export class Invite<Omitted extends keyof APIActualInvite | '' = 'created_at' | 'expires_at'> extends Structure<
APIActualInvite,
Omitted
> {
/**
* The template used for removing data from the raw data stored for each Invite
*
* @remarks This template has defaults, if you want to remove additional data and keep the defaults,
* use `Object.defineProperties`. To override the defaults, set this value directly.
*/
public static override readonly DataTemplate: Partial<APIActualInvite> = {
set created_at(_: string) {},
set expires_at(_: string) {},
};
/**
* Optimized storage of {@link discord-api-types/v10#(APIActualInvite:interface).expires_at}
*
* @internal
*/
protected [kExpiresTimestamp]: number | null = null;
/**
* Optimized storage of {@link discord-api-types/v10#(APIActualInvite:interface).created_at}
*
* @internal
*/
protected [kCreatedTimestamp]: number | null = null;
/**
* @param data - The raw data received from the API for the invite
*/
public constructor(data: Partialize<APIActualInvite, Omitted>) {
super(data);
this.optimizeData(data);
}
/**
* {@inheritDoc Structure.[kPatch]}
*
* @internal
*/
public override [kPatch](data: Partial<APIActualInvite>) {
super[kPatch](data);
return this;
}
/**
* {@inheritDoc Structure.optimizeData}
*
* @internal
*/
protected override optimizeData(data: Partial<APIActualInvite>) {
if (data.expires_at) {
this[kExpiresTimestamp] = Date.parse(data.expires_at);
}
if (data.created_at) {
this[kCreatedTimestamp] = Date.parse(data.created_at);
}
}
/**
* The code for this invite
*/
public get code() {
return this[kData].code;
}
/**
* The target type (for voice channel invites)
*/
public get targetType() {
return this[kData].target_type;
}
/**
* The type of this invite
*/
public get type() {
return this[kData].type;
}
/**
* The approximate number of online members of the guild this invite is for
*
* @remarks Only available when the invite was fetched from `GET /invites/<code>` with counts
*/
public get approximatePresenceCount() {
return this[kData].approximate_presence_count;
}
/**
* The approximate total number of members of the guild this invite is for
*
* @remarks Only available when the invite was fetched from `GET /invites/<code>` with counts
*/
public get approximateMemberCount() {
return this[kData].approximate_member_count;
}
/**
* The timestamp this invite will expire at
*/
public get expiresTimestamp() {
if (this[kExpiresTimestamp]) {
return this[kExpiresTimestamp];
}
const createdTimestamp = this.createdTimestamp;
const maxAge = this.maxAge;
if (createdTimestamp && maxAge) {
this[kExpiresTimestamp] = createdTimestamp + (maxAge as number) * 1_000;
}
return this[kExpiresTimestamp];
}
/**
* The time the invite will expire at
*/
public get expiresAt() {
const expiresTimestamp = this.expiresTimestamp;
return expiresTimestamp ? new Date(expiresTimestamp) : null;
}
/**
* The number of times this invite has been used
*/
public get uses() {
return this[kData].uses;
}
/**
* The maximum number of times this invite can be used
*/
public get maxUses() {
return this[kData].max_uses;
}
/**
* The maximum age of the invite, in seconds, 0 for non-expiring
*/
public get maxAge() {
return this[kData].max_age;
}
/**
* Whether this invite only grants temporary membership
*/
public get temporary() {
return this[kData].temporary;
}
/**
* The timestamp this invite was created at
*/
public get createdTimestamp() {
return this[kCreatedTimestamp];
}
/**
* The time the invite was created at
*/
public get createdAt() {
const createdTimestamp = this.createdTimestamp;
return createdTimestamp ? new Date(createdTimestamp) : null;
}
/**
* The URL to the invite
*/
public get url() {
return this.code ? `${RouteBases.invite}/${this.code}` : null;
}
/**
* When concatenated with a string, this automatically concatenates the invite's URL instead of the object.
*
* @returns The URL to the invite or an empty string if it doesn't have a code
*/
public override toString() {
return this.url ?? '';
}
/**
* {@inheritDoc Structure.toJSON}
*/
public override toJSON() {
const clone = super.toJSON();
if (this[kExpiresTimestamp]) {
clone.expires_at = new Date(this[kExpiresTimestamp]).toISOString();
}
if (this[kCreatedTimestamp]) {
clone.created_at = new Date(this[kCreatedTimestamp]).toISOString();
}
return clone;
}
/**
* Returns the primitive value of the specified object.
*/
public override valueOf() {
return this.code ?? super.valueOf();
}
}

View File

@@ -0,0 +1 @@
export * from './Invite.js';

View File

@@ -0,0 +1,40 @@
import type { APIAvatarDecorationData } from 'discord-api-types/v10';
import { Structure } from '../Structure.js';
import { kData } from '../utils/symbols.js';
import type { Partialize } from '../utils/types.js';
/**
* Represents metadata of an avatar decoration of a User.
*
* @typeParam Omitted - Specify the properties that will not be stored in the raw data field as a union, implement via `DataTemplate`
*/
export class AvatarDecorationData<Omitted extends keyof APIAvatarDecorationData | '' = ''> extends Structure<
APIAvatarDecorationData,
Omitted
> {
/**
* The template used for removing data from the raw data stored for each Connection
*/
public static override readonly DataTemplate: Partial<APIAvatarDecorationData> = {};
/**
* @param data - The raw data received from the API for the connection
*/
public constructor(data: Partialize<APIAvatarDecorationData, Omitted>) {
super(data);
}
/**
* The id of the SKU this avatar decoration is part of.
*/
public get skuId() {
return this[kData].sku_id;
}
/**
* The asset of this avatar decoration.
*/
public get asset() {
return this[kData].asset;
}
}

View File

@@ -0,0 +1,95 @@
import type { APIConnection } from 'discord-api-types/v10';
import { Structure } from '../Structure.js';
import { kData, kPatch } from '../utils/symbols.js';
import type { Partialize } from '../utils/types.js';
/**
* Represents a user's connection on Discord.
*
* @typeParam Omitted - Specify the properties that will not be stored in the raw data field as a union, implement via `DataTemplate`
*/
export class Connection<Omitted extends keyof APIConnection | '' = ''> extends Structure<APIConnection, Omitted> {
/**
* The template used for removing data from the raw data stored for each Connection
*/
public static override readonly DataTemplate: Partial<APIConnection> = {};
/**
* @param data - The raw data received from the API for the connection
*/
public constructor(data: Partialize<APIConnection, Omitted>) {
super(data);
}
/**
* {@inheritDoc Structure.[kPatch]}
*
* @internal
*/
public override [kPatch](data: Partial<APIConnection>) {
return super[kPatch](data);
}
/**
* The id of the connection account
*/
public get id() {
return this[kData].id;
}
/**
* The username of the connection account
*/
public get name() {
return this[kData].name;
}
/**
* The type of service this connection is for
*/
public get type() {
return this[kData].type;
}
/**
* Whether the connection is revoked
*/
public get revoked() {
return this[kData].revoked ?? false;
}
/**
* Whether the connection is verified
*/
public get verified() {
return this[kData].verified;
}
/**
* Whether friend sync is enabled for this connection
*/
public get friendSync() {
return this[kData].friend_sync;
}
/**
* Whether activities related to this connection are shown in the users presence
*/
public get showActivity() {
return this[kData].show_activity;
}
/**
* Whether this connection has an Oauth2 token for console voice transfer
*/
public get twoWayLink() {
return this[kData].two_way_link;
}
/**
* The visibility state for this connection
*/
public get visibility() {
return this[kData].visibility;
}
}

View File

@@ -0,0 +1,180 @@
import { DiscordSnowflake } from '@sapphire/snowflake';
import type { APIUser } from 'discord-api-types/v10';
import { Structure } from '../Structure.js';
import { kData, kPatch } from '../utils/symbols.js';
import { isIdSet } from '../utils/type-guards.js';
import type { Partialize } from '../utils/types.js';
/**
* Represents any user on Discord.
*
* @typeParam Omitted - Specify the properties that will not be stored in the raw data field as a union, implement via `DataTemplate`
* @remarks has a substructure `AvatarDecorationData`, which needs to be instantiated and stored by an extending class using it
*/
export class User<Omitted extends keyof APIUser | '' = ''> extends Structure<APIUser, Omitted> {
/**
* The template used for removing data from the raw data stored for each User
*/
public static override readonly DataTemplate: Partial<APIUser> = {};
/**
* @param data - The raw data received from the API for the user
*/
public constructor(data: Partialize<APIUser, Omitted>) {
super(data);
}
/**
* {@inheritDoc Structure.[kPatch]}
*
* @internal
*/
public override [kPatch](data: Partial<APIUser>) {
return super[kPatch](data);
}
/**
* The user's id
*/
public get id() {
return this[kData].id;
}
/**
* The username of the user
*/
public get username() {
return this[kData].username;
}
/**
* The user's 4 digit tag, if a bot
*/
public get discriminator() {
return this[kData].discriminator;
}
/**
* The user's display name, the application name for bots
*/
public get globalName() {
return this[kData].global_name;
}
/**
* The name displayed in the client for this user when no nickname is set
*/
public get displayName() {
return this.globalName ?? this.username;
}
/**
* The user avatar's hash
*/
public get avatar() {
return this[kData].avatar;
}
/**
* Whether the user is a bot
*/
public get bot() {
return this[kData].bot ?? false;
}
/**
* Whether the user is an Official Discord System user
*/
public get system() {
return this[kData].system ?? false;
}
/**
* Whether the user has mfa enabled
*
* @remarks This property is only set when the user was fetched with an OAuth2 token and the `identify` scope
*/
public get mfaEnabled() {
return this[kData].mfa_enabled;
}
/**
* The user's banner hash
*
* @remarks This property is only set when the user was manually fetched
*/
public get banner() {
return this[kData].banner;
}
/**
* The base 10 accent color of the user's banner
*
* @remarks This property is only set when the user was manually fetched
*/
public get accentColor() {
return this[kData].accent_color;
}
/**
* The user's primary Discord language
*
* @remarks This property is only set when the user was fetched with an Oauth2 token and the `identify` scope
*/
public get locale() {
return this[kData].locale;
}
/**
* Whether the email on the user's account has been verified
*
* @remarks This property is only set when the user was fetched with an OAuth2 token and the `email` scope
*/
public get verified() {
return this[kData].verified;
}
/**
* The user's email
*
* @remarks This property is only set when the user was fetched with an OAuth2 token and the `email` scope
*/
public get email() {
return this[kData].email;
}
/**
* The type of nitro subscription on the user's account
*
* @remarks This property is only set when the user was fetched with an OAuth2 token and the `identify` scope
*/
public get premiumType() {
return this[kData].premium_type;
}
/**
* The timestamp the user was created at
*/
public get createdTimestamp() {
return isIdSet(this.id) ? DiscordSnowflake.timestampFrom(this.id) : null;
}
/**
* The time the user was created at
*/
public get createdAt() {
const createdTimestamp = this.createdTimestamp;
return createdTimestamp ? new Date(createdTimestamp) : null;
}
/**
* The hexadecimal version of the user accent color, with a leading hash
*
* @remarks This property is only set when the user was manually fetched
*/
public get hexAccentColor() {
const accentColor = this.accentColor;
if (typeof accentColor !== 'number') return accentColor;
return `#${accentColor.toString(16).padStart(6, '0')}`;
}
}

View File

@@ -0,0 +1,3 @@
export * from './AvatarDecorationData.js';
export * from './User.js';
export * from './Connection.js';

View File

@@ -0,0 +1,10 @@
export function extendTemplate<SuperTemplate extends Record<string, unknown>>(
superTemplate: SuperTemplate,
additions: Record<string, unknown>,
): Record<string, unknown> & SuperTemplate {
return Object.defineProperties(additions, Object.getOwnPropertyDescriptors(superTemplate)) as Record<
string,
unknown
> &
SuperTemplate;
}

View File

@@ -0,0 +1,15 @@
export const kData = Symbol.for('djs.structures.data');
export const kClone = Symbol.for('djs.structures.clone');
export const kPatch = Symbol.for('djs.structures.patch');
export const kExpiresTimestamp = Symbol.for('djs.structures.expiresTimestamp');
export const kCreatedTimestamp = Symbol.for('djs.structures.createdTimestamp');
export const kEditedTimestamp = Symbol.for('djs.structures.editedTimestamp');
export const kArchiveTimestamp = Symbol.for('djs.structures.archiveTimestamp');
export const kAllow = Symbol.for('djs.structures.allow');
export const kDeny = Symbol.for('djs.structures.deny');
export const kLastPinTimestamp = Symbol.for('djs.structures.lastPinTimestamp');
export const kMixinConstruct = Symbol.for('djs.structures.mixin.construct');
export const kMixinToJSON = Symbol.for('djs.structures.mixin.toJSON');

View File

@@ -0,0 +1,3 @@
export function isIdSet(id: unknown): id is bigint | string {
return typeof id === 'string' || typeof id === 'bigint';
}

View File

@@ -0,0 +1,36 @@
export type ReplaceOmittedWithUnknown<Omitted extends keyof Data | '', Data> = {
[Key in keyof Data]: Key extends Omitted ? unknown : Data[Key];
};
export type CollapseUnion<Type> = Type extends infer Union ? { [Key in keyof Union]: Union[Key] } : never;
export type OptionalPropertyNames<Type> = {
[Key in keyof Type]-?: {} extends { [Prop in Key]: Type[Key] } ? Key : never;
}[keyof Type];
export type MergePrototype<Class1, Class2> = Pick<Class1, Exclude<keyof Class1, keyof Class2>> &
Pick<Class2, Exclude<keyof Class2, OptionalPropertyNames<Class2>>> &
Pick<Class2, Exclude<OptionalPropertyNames<Class2>, keyof Class1>> & {
[Prop in OptionalPropertyNames<Class2> & keyof Class1]: Class1[Prop] | Exclude<Class2[Prop], undefined>;
};
export type MergePrototypes<ClassArray extends readonly unknown[]> = ClassArray extends [infer Class1]
? Class1
: ClassArray extends [infer Class1, ...infer Rest]
? MergePrototype<Class1, MergePrototypes<Rest>>
: never;
export interface RecursiveReadonlyArray<ItemType> extends ReadonlyArray<ItemType | RecursiveReadonlyArray<ItemType>> {}
export type EnumLike<Enum, Value> = Record<keyof Enum, Value>;
export type If<Check, Value, True, False = never> = Check extends Value ? (Value extends Check ? True : False) : False;
export type NonAbstract<Type extends abstract new (...args: any) => any> = Type extends abstract new (
...args: infer Args
) => infer Instance
? Pick<Type, keyof Type> & (new (...args: Args) => Instance)
: never;
export type Partialize<Type, Omitted extends keyof Type | ''> = Omit<Type, Omitted> &
Partial<Pick<Type, Exclude<Omitted, ''>>>;